package craneding.gwt.databean.rebind;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.Properties;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import craneding.gwt.databean.client.model.AbstractBeanReflection;
import craneding.gwt.databean.client.model.BeanInfo;
import craneding.gwt.databean.client.model.Category;
import craneding.gwt.databean.client.model.DynamicBean;
import craneding.gwt.databean.client.model.FieldInfo;
import craneding.gwt.databean.client.model.FieldInfoX;
import craneding.gwt.databean.client.model.FieldInfoXes;
import craneding.gwt.databean.client.model.ReflectibleBean;
import craneding.gwt.databean.client.model.FieldInfo.UI;

/**
 * Generates an implementation of a user-defined interface <code>T</code> that
 * extends {@link ReflectibleBean}.
 * 
 * @author crane.ding
 * @version 1.0 2008-11-27
 */
public class ReflectibleBeanGenerator extends Generator {

	private static enum CodeParameter {
		userClass, staticInfo, userBeanName, getField, setField, fieldNames
	}

	@Override
	public String generate(TreeLogger logger, GeneratorContext context,
			String typeName) throws UnableToCompleteException {

		TypeOracle typeOracle = context.getTypeOracle();

		// Get metadata describing the user's class.
		JClassType userType = getValidUserType(logger, typeName, typeOracle);

		JField[] fields = userType.getFields();

		String resultName = generateImplClass(logger, context, userType, fields);

		return resultName;
	}

	private String generateImplClass(TreeLogger logger,
			GeneratorContext context, JClassType userType, JField[] fields)
			throws UnableToCompleteException {
		String tempfile = "generatetemplate.vm";
		String base = ReflectibleBeanGenerator.class.getResource("").getPath();

		Properties prop = new Properties();
		prop.put("resource.loade", "file");
		prop.put("file.resource.loader.path", base);

		VelocityContext vContext = null;
		Template template = null;
		try {
			Velocity.init(prop);
			vContext = new VelocityContext();
			template = Velocity.getTemplate(tempfile, "UTF8");
		} catch (Exception e) {
			logger.log(TreeLogger.ERROR,
					"Velocity init error or not find template file.", e);
			throw new UnableToCompleteException();
		}
		String tab = "\n\t\t";
		String ucFullName = userType.getQualifiedSourceName();
		// get a bean info set str
		String bi = getAnnotationBeanInfo(userType);
		// get a field info set set
		StringBuffer fi = new StringBuffer(tab);
		// new shared_fieldInfos
		fi.append("shared_fieldInfos = new HashMap<String, FieldInfo>();");
		// get field str
		StringBuffer gi = new StringBuffer("");
		// set field str
		StringBuffer si = new StringBuffer("");
		si.append("\n\t\tif(old==null&&value==null)");
		si.append(tab).append("\treturn;");
		si.append("\n\t\tif(old!=null&&value!=null&&old.toString().equals(value.toString()))");
		si.append(tab).append("\treturn;\n\n\t\t");
		// field names str
		StringBuffer fn = new StringBuffer("");
		// get a FieldInfoXes set str
		StringBuffer fx = new StringBuffer(tab);
		fx.append("shared_fieldInfoXes = new HashMap<String, FieldInfoXes>();");
		for (JField field : fields) {
			fi.append(tab + "shared_fieldInfos.put(\"").append(field.getName());
			fi.append("\", new FieldMetaData()");
			fi.append(getAnnotationFieldInfo(logger, field)).append(");");
			gi.append(getField(field, userType));
			si.append(setField(field));

			if (fn.length() > 0)
				fn.append(",");
			fn.append("\"").append(field.getName()).append("\"");

			final String str = getFieldInfoXes(field);
			if (!str.equals("")) {
				fx.append("\n\t\tshared_fieldInfoXes.put(\"");
				fx.append(field.getName());
				fx.append("\", new FieldInfoXesMetaData()");
				fx.append(".value(").append(str).append(")").append(");");
			}
		}
		// Compute the package and class names of the generated class.
		String pkgName = userType.getPackage().getName();
		String subName = computeSubclassName(userType);

		// put user's class
		vContext.put(CodeParameter.userClass.name(), ucFullName);
		// put static info
		vContext.put(CodeParameter.staticInfo.name(), bi + fi.toString()
				+ fx.toString());
		// put user class name
		vContext.put(CodeParameter.userBeanName.name(), subName);
		vContext.put(CodeParameter.getField.name(), gi.toString());
		vContext.put(CodeParameter.setField.name(), si.toString());
		vContext.put(CodeParameter.fieldNames.name(), fn.toString());

		StringWriter reader = new StringWriter();
		try {
			template.merge(vContext, reader);
		} catch (Exception e) {
			logger.log(TreeLogger.ERROR, "template merge error.", e);
			throw new UnableToCompleteException();
		}
		String code = reader.toString();

		// Begin writing the generated source.
		ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
				pkgName, subName);
		f.addImport("java.util.*");
		f.addImport(DynamicBean.class.getPackage().getName() + ".*");
		f.setSuperclass(AbstractBeanReflection.class.getSimpleName());

		PrintWriter pw = context.tryCreate(logger, pkgName, subName);
		if (pw != null) {
			SourceWriter sw = f.createSourceWriter(context, pw);
			sw.println(code);

			// Finish.
			sw.commit(logger);
			pw.close();
		}

		return f.getCreatedClassName();
	}

	private String getFieldInfoXes(JField field) {
		FieldInfoXes infoXes = field.getAnnotation(FieldInfoXes.class);
		if (infoXes == null)
			return "";
		FieldInfoX[] value = infoXes.value();
		if (value == null)
			return "";

		StringBuffer code = new StringBuffer("");
		for (FieldInfoX infoX : value) {
			if (code.length() > 0) {
				code.append(",");
			}
			code.append("new FieldInfoXMetaData().name(\"");
			code.append(infoX.name()).append("\").value(\"");
			code.append(infoX.value()).append("\")");
		}
		return code.toString();
	}

	private Object setField(JField field) {
		StringBuffer code = new StringBuffer();
		String name = field.getName();
		String upperFirst = name.substring(0, 1).toUpperCase().concat(
				name.substring(1));
		JType type = field.getType();
		code.append("if(\"").append(name).append("\".equals(field))");
		code.append("\n\t\t\tdelegator.set").append(upperFirst);
		code.append("((");
		if (type.getClass().getName().equals(byte.class.getName()))
			code.append(Byte.class.getName());
		else if (type.getQualifiedSourceName().equals(char.class.getName()))
			code.append(Character.class.getName());
		else if (type.getQualifiedSourceName().equals(short.class.getName()))
			code.append(Short.class.getName());
		else if (type.getQualifiedSourceName().equals(long.class.getName()))
			code.append(Long.class.getName());
		else if (type.getQualifiedSourceName().equals(int.class.getName()))
			code.append(Integer.class.getName());
		else if (type.getQualifiedSourceName().equals(float.class.getName()))
			code.append(Float.class.getName());
		else if (type.getQualifiedSourceName().equals(double.class.getName()))
			code.append(Double.class.getName());
		else if (type.getQualifiedSourceName().equals(boolean.class.getName()))
			code.append(Boolean.class.getName());
		else
			code.append(type.getQualifiedSourceName());
		code.append(")value);\n\t\t");
		return code.toString();
	}

	private String getField(JField field, JClassType userType) {
		StringBuffer code = new StringBuffer();
		String name = field.getName();
		String upperFirst = name.substring(0, 1).toUpperCase().concat(
				name.substring(1));
		code.append("if(\"").append(name).append("\".equals(field))");
		code.append("\n\t\t\treturn delegator.");
		JMethod method = null;
		try {
			method = userType.getMethod("get" + upperFirst, new JType[] {});
		} catch (NotFoundException e) {
		}
		if (method != null) {
			code.append("get" + upperFirst);
			return code.append("();\n\t\t").toString();
		}
		try {
			method = userType.getMethod("is" + upperFirst, new JType[] {});
		} catch (NotFoundException e) {
		}
		if (method != null) {
			code.append("is" + upperFirst);
			return code.append("();\n\t\t").toString();
		}
		return "";
	}

	private String getAnnotationFieldInfo(TreeLogger logger, JField field)
			throws UnableToCompleteException {
		StringBuffer code = new StringBuffer();
		FieldInfo info = field.getAnnotation(FieldInfo.class);
		if (!info.label().equals(""))
			code.append(concat("label", info.label(), true));
		if (!info.regexp().equals(""))
			code.append(concat("regexp", info.regexp(), true));
		if (!info.description().equals(""))
			code.append(concat("description", info.description(), true));
		if (!info.icon().equals(""))
			code.append(concat("icon", info.icon(), true));
		if (!info.category().equals(""))
			code.append(concat("category", info.category(), true));
		if (!info.datatype().getName().equals(Object.class.getName())) {
			if (!info.datatype().getName().equals(
					field.getType().getQualifiedSourceName())) {
				logger.log(TreeLogger.ERROR, field.getName()
						+ " field must be "
						+ field.getType().getQualifiedSourceName() + " type.");
				throw new UnableToCompleteException();
			}
			if (!info.datatype().getName().equals(String.class.getName())) {
				final String str = field.getType().getQualifiedSourceName()
						+ ".class";
				code.append(concat("datatype", str, false));
			}
		} else if (!field.getType().getQualifiedSourceName().equals(
				String.class.getName())) {
			final String str = field.getType().getQualifiedSourceName()
					+ ".class";
			code.append(concat("datatype", str, false));
		}

		if (!info.enumKey().getName().equals(Object.class.getName())) {
			final String str = info.enumKey().getName() + ".class";
			code.append(concat("enumKey", str, false));
		}
		if (field.getType() instanceof JEnumType) {
			JEnumType enumType = (JEnumType) field.getType();
			code.append(concat("enumerations", enumType
					.getQualifiedSourceName()
					+ ".values()", false));
		}
		if (info.required())
			code.append(concat("required", info.required() + "", false));
		if (info.ui().equals(UI.defaultUI)) {
			String str = "FieldInfo.UI.";
			JType type = field.getType();
			String sourceName = type.getQualifiedSourceName();
			if (type instanceof JEnumType) {
				str = str.concat(UI.select.name());
				code.append(concat("ui", str, false));
			} else if (sourceName.equals(Date.class.getName())) {
				str = str.concat(UI.calendar.name());
				code.append(concat("ui", str, false));
			} else if (sourceName.equals(byte.class.getName())
					|| sourceName.equals(Byte.class.getName())
					|| sourceName.equals(short.class.getName())
					|| sourceName.equals(Short.class.getName())
					|| sourceName.equals(int.class.getName())
					|| sourceName.equals(Integer.class.getName())
					|| sourceName.equals(long.class.getName())
					|| sourceName.equals(Long.class.getName())
					|| sourceName.equals(float.class.getName())
					|| sourceName.equals(Float.class.getName())
					|| sourceName.equals(double.class.getName())
					|| sourceName.equals(Double.class.getName())) {
				str = str.concat(UI.numbertext.name());
				code.append(concat("ui", str, false));
			}
		} else if (!info.ui().equals(UI.text)) {
			final String str = "FieldInfo.UI." + info.ui().name();
			code.append(concat("ui", str, false));
		}
		if (info.length() != -1)
			code.append(concat("length", info.length() + "", false));
		if (info.fractions() != -1)
			code.append(concat("fractions", info.fractions() + "", false));
		if (!info.createEditable()) {
			final String str = info.createEditable() + "";
			code.append(concat("createEditable", str, false));
		}
		if (!info.updateEditable()) {
			final String str = info.updateEditable() + "";
			code.append(concat("updateEditable", str, false));
		}
		return code.toString();
	}

	private String getAnnotationBeanInfo(JClassType userType) {
		StringBuffer code = new StringBuffer("");
		if (userType.isAnnotationPresent(BeanInfo.class)) {
			BeanInfo annotation = userType.getAnnotation(BeanInfo.class);
			// bean name
			String name = annotation.name();
			// display name
			String label = annotation.label();
			// description
			String description = annotation.description();
			// defalut icon
			String icon = annotation.icon();
			// Bean's Category[]
			Category[] categories = annotation.categories();
			if (name.length() > 0)
				code.append(concat("name", name, true));
			if (label.length() > 0)
				code.append(concat("label", label, true));
			if (description.length() > 0)
				code.append(concat("description", description, true));
			if (icon.length() > 0)
				code.append(concat("icon", icon, true));
			if (categories != null && categories.length > 0) {
				StringBuffer str = new StringBuffer("");
				for (Category category : categories) {
					String value;
					StringBuffer cr = new StringBuffer("");
					if ((value = category.name()).length() > 0)
						cr.append(concat("name", value, true));
					else if ((value = category.dependField()).length() > 0)
						cr.append(concat("dependField", value, true));
					else if ((value = category.dependValue()).length() > 0)
						cr.append(concat("dependValue", value, true));
					if (value.length() > 0) {
						cr.insert(0, ",new CategoryMetadata()");
						str.append(cr);
					}
				}
				if (str.length() > 0) {
					String s = "new CategoryMetadata[]{";
					s = s.concat(str.substring(1)).concat("}");
					code.append(concat("categories", s, false));
				}
			}
		}
		if (code.toString().length() <= 0)
			return "";
		code.insert(0, "shared_beanInfo = new BeanMetadata()").append(";");
		return code.toString();
	}

	private String concat(String name, String value, boolean asString) {
		// e.g. .name("value")
		if (asString)
			return "." + name + "(\"" + value + "\")";
		else
			return "." + name + "(" + value + ")";
	}

	private String computeSubclassName(JClassType userType) {
		String baseName = userType.getName().replace('.', '_');
		return baseName + "_BeanReflection";
	}

	private JClassType getValidUserType(TreeLogger logger, String typeName,
			TypeOracle typeOracle) throws UnableToCompleteException {
		try {
			// Get the type that the user is introducing.
			JClassType userType = typeOracle.getType(typeName);

			// Get the type this generator is designed to support.
			String name = ReflectibleBean.class.getName();
			JClassType magicType = typeOracle.findType(name.replace('$', '.'));

			// Ensure it's an interface.
			if (userType.isClass() == null) {
				logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
						+ " must be an class", null);
				throw new UnableToCompleteException();
			}

			// Ensure proper derivation.
			if (!userType.isAssignableTo(magicType)) {
				logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
						+ " must be assignable to "
						+ magicType.getQualifiedSourceName(), null);
				throw new UnableToCompleteException();
			}

			return userType;

		} catch (NotFoundException e) {
			logger.log(TreeLogger.ERROR, "Unable to find required type(s)", e);
			throw new UnableToCompleteException();
		}
	}

}
